Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

7장. 트랜잭션과 데이터 일관성

1. 하나의 작업처럼 보이지만

서비스에서는 여러 데이터 변경이
하나의 작업처럼 묶이는 경우가 많다.

예를 들어 주문을 생성하는 로직을 보면 다음과 같다.

await orderRepository.create(order);
await userRepository.decreasePoint(userId, amount);

이 코드는 자연스럽게 읽힌다.

  • 주문을 생성하고
  • 포인트를 차감한다

👉 하나의 작업처럼 보인다


2. 하지만 실제로는 두 개의 작업이다

문제는 이 두 작업이
서로 다른 DB 작업이라는 점이다.

만약 다음과 같은 상황이 발생하면 어떻게 될까?

await orderRepository.create(order);     // 성공
await userRepository.decreasePoint(...); // 실패

👉 결과

  • 주문은 생성됨
  • 포인트는 차감되지 않음

3. 데이터가 깨지는 순간

이 상태는 비즈니스적으로 문제가 된다.

  • 돈은 안 빠졌는데 주문은 완료됨
  • 혹은 반대로 돈만 빠지고 주문 실패

👉 데이터 일관성이 깨진다


4. 그래서 트랜잭션이 필요하다

이 문제를 해결하기 위해 등장한 개념이
👉 트랜잭션(Transaction) 이다

트랜잭션은 여러 작업을 하나로 묶는다.

await db.transaction(async (trx) => {
  await orderRepository.create(order, trx);
  await userRepository.decreasePoint(userId, amount, trx);
});

👉 결과

  • 둘 다 성공 → 반영
  • 하나라도 실패 → 전체 롤백

5. 그런데 구조를 나누면 문제가 생긴다

6장에서 구조를 이렇게 나눴다.

Controller → Service → Repository

이 구조에서 트랜잭션은 어디에서 관리해야 할까?


6. 자연스럽게 잘못된 선택

처음에는 Repository에서 처리하고 싶어진다.

class OrderRepository {
  async create(order) {
    await db.transaction(async () => {
      await db.query(...);
    });
  }
}

👉 하지만 이 구조는 문제가 있다

  • 다른 작업과 묶을 수 없다
  • 트랜잭션이 분리된다

👉 결국 의미가 없다


7. 해결은 Service에 있다

트랜잭션은 “데이터 저장”이 아니라
👉 “작업의 흐름”을 기준으로 묶어야 한다

그래서 Service에서 관리한다.

await db.transaction(async (trx) => {
  await orderRepository.create(order, trx);
  await userRepository.decreasePoint(userId, amount, trx);
});

👉 핵심

  • Service → 전체 흐름 제어
  • Repository → 실행만 담당

8. Repository는 트랜잭션을 전달받는다

class UserRepository {
  async decreasePoint(userId, amount, trx?) {
    return trx
      ? trx.query(...)
      : db.query(...);
  }
}

👉 트랜잭션이 있으면 사용
👉 없으면 기본 DB 사용


9. 핵심은 위치다

트랜잭션을 어디에 두느냐가 중요하다.

위치결과
Repository분리됨 (문제 발생)
Service하나로 묶임 (정상)

10. 정리

이 장의 핵심은 단순하다.

👉 트랜잭션은 DB 기능이지만
👉 DB 레이어에서 관리하면 안 된다